package com.fortune.cat.plugins;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fortune.cat.Exception.CustomizeException;
import com.fortune.cat.bean.TokenVerifyConfig;
import com.google.gson.Gson;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.List;
import java.util.Optional;

import static com.google.common.net.HttpHeaders.AUTHORIZATION;

/**
 * @author zhaozeyang
 * @date 2019/4/3 11:10
 * @description
 */
//@Slf4j
//@Component
public class TokenVerifyPlugin extends AbstractFortuneCatPlugin<TokenVerifyConfig> {

    private final Logger log = LoggerFactory.getLogger(TokenVerifyPlugin.class);

    public TokenVerifyPlugin(ReactiveStringRedisTemplate reactiveStringRedisTemplate,
                             StringRedisTemplate stringRedisTemplate,
                             LoadBalancerClient loadBalancer) {
        super(TokenVerifyConfig.class);
        this.reactiveStringRedisTemplate = reactiveStringRedisTemplate;
        this.stringRedisTemplate = stringRedisTemplate;
        this.loadBalancer = loadBalancer;
    }


    private final Integer TOKEN_INVALID_STATUS = 30001;

    private final Integer SUCCESS = 0;

    private final Gson gson = new Gson();

    private final ReactiveStringRedisTemplate reactiveStringRedisTemplate;

    private final StringRedisTemplate stringRedisTemplate;

    protected final LoadBalancerClient loadBalancer;

    private static final WebClient WEB_CLIENT = WebClient.create();

    @Override
    protected Mono<Void> doApply(TokenVerifyConfig config, ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("TokenVerifyFilterFactory | token is {}", Optional.ofNullable(exchange.getRequest().getHeaders().getFirst(AUTHORIZATION)).orElse("token is empty"));
        return verifyTokenAndGetUserInfo(exchange, config).flatMap(chain::filter);
    }

    private Mono<ServerWebExchange> verifyTokenAndGetUserInfo(ServerWebExchange exchange, TokenVerifyConfig config) {
        String routeId = ((Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR)).getId();
        //获取用户中心地址
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst(AUTHORIZATION);
        if (StringUtils.isBlank(token)) {
            throwError(HttpStatus.OK, "token is null,must have token", TOKEN_INVALID_STATUS);
        }
        //判断是游客还是用户
        String[] tokenArr = token.split("\\s+");
        boolean isTourist = "TouristBearer".equals(tokenArr[0]);
        //先从缓存里取,如果为空再请求用户中心
        return getUserInfoResponseByCache(token, config, routeId).switchIfEmpty(getUserInfo(config, token, tokenArr[1], isTourist, request, routeId))
                .map(p -> installUserInfo(p, exchange));
    }

    @Override
    protected int doGetOrder() {
        return 0;
    }

    @Override
    protected boolean skipPresent(TokenVerifyConfig config, ServerWebExchange exchange) {
        return config.getSkipUri().contains(exchange.getRequest().getPath().toString());
    }

    /**
     * 调用用户中心校验token
     *
     * @param config
     * @param token
     * @param realToken
     * @param isTourist
     * @param request
     * @param routeId
     * @return
     */
    private Mono<UserInfo> getUserInfo(TokenVerifyConfig config, String token, String realToken, boolean isTourist, ServerHttpRequest request, String routeId) {
        final ServiceInstance instance = loadBalancer.choose("backstageUser");
        if (instance == null) {
            throwError(HttpStatus.INTERNAL_SERVER_ERROR, "no instance for backstageUser", TOKEN_INVALID_STATUS);
        }
        ParameterizedTypeReference typeReference = new ParameterizedTypeReference<CommonResponse<UserInfoResponse.Data>>() {
        };
        ValidateParam validateParam = new ValidateParam();
        Optional.ofNullable(request.getHeaders().getFirst("v")).filter(StringUtils::isNotBlank).ifPresent(validateParam::setClientVersion);
        Optional.ofNullable(realToken).filter(StringUtils::isNotBlank).ifPresent(validateParam::setAccessToken);
        Optional.ofNullable(request.getHeaders().getFirst("devid")).filter(StringUtils::isNotBlank).ifPresent(validateParam::setDeviceId);
        String overrideScheme = instance.isSecure() ? "https" : "http";
        String bashUri = overrideScheme.concat("://").concat(instance.getHost()).concat(":") + instance.getPort();
        String requestUri = bashUri.concat("/tourist");
        if (isTourist) {
            return WEB_CLIENT.get().uri(requestUri)
                    .header(HttpHeaders.AUTHORIZATION, token)
                    .exchange()
                    .doOnError(e -> {
                        log.info("verifyTokenAndGetUserInfo | tourist user error is ", e);
                        throwError(HttpStatus.INTERNAL_SERVER_ERROR, "webclient transfer error", HttpStatus.INTERNAL_SERVER_ERROR.value());
                    })
                    .timeout(Duration.ofMillis(config.getOutTime()))
                    .flatMap(clientResponse -> {
                        if (!clientResponse.statusCode().is2xxSuccessful() && !clientResponse.statusCode().is4xxClientError()) {
                            throwError(HttpStatus.INTERNAL_SERVER_ERROR, "backstage user error", HttpStatus.INTERNAL_SERVER_ERROR.value());
                        }
                        return clientResponse.bodyToMono(UserInfo.class);
                    }).doOnSuccess(p -> createUserInfoCache(token, p, config, routeId));
        }
        requestUri = bashUri.concat("/userCenter/getUserInfoByToken");
        return WEB_CLIENT.post().uri(requestUri)
                //填充header
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
                //request body
                .body(Mono.just(validateParam), ValidateParam.class)
                //exchange 可以获得原始response
                .exchange()
                //自身异常处理
                .doOnError(e -> {
                    log.info("verifyTokenAndGetUserInfo | normal user  error is ", e);
                    throwError(HttpStatus.INTERNAL_SERVER_ERROR, "webclient transfer error", HttpStatus.INTERNAL_SERVER_ERROR.value());
                })
                //超时时间
                .timeout(Duration.ofMillis(config.getOutTime()))
                //转换response
                .flatMap(clientResponse -> {
                    if (!clientResponse.statusCode().is2xxSuccessful()) {
                        throwError(HttpStatus.INTERNAL_SERVER_ERROR, "backstage user error", HttpStatus.INTERNAL_SERVER_ERROR.value());
                    }
                    return clientResponse.bodyToMono(typeReference);
                }).map(commonResponse -> {
                    CommonResponse<UserInfoResponse.Data> response = (CommonResponse<UserInfoResponse.Data>) commonResponse;
                    if (!SUCCESS.equals(response.getStatus())) {
                        throwError(HttpStatus.OK, response.getMsg(), response.getStatus());
                    }
                    UserInfoResponse.Data userInfoResponse = response.getData();
                    UserInfoResponse.Bussion business = userInfoResponse.getBussion();
                    return new UserInfo(isTourist, business.getStuId(), business.getStuUid(), userInfoResponse.getAreaCode(), business.getStuGradeId(), null);
                }).doOnNext(p -> createUserInfoCache(token, (UserInfo) p, config, routeId));
    }

    private void throwError(HttpStatus httpStatus, String message, Integer status) {
        throw TokenVerifyException.create(httpStatus, message, status);
    }

    public static class TokenVerifyException extends CustomizeException {


        TokenVerifyException(HttpStatus httpStatus, Integer status, String message) {
            super(message, status, httpStatus);
        }

        static TokenVerifyException create(HttpStatus httpStatus, String message, Integer status) {
            return new TokenVerifyException(httpStatus, status, message);
        }
    }


    /**
     * 通过缓存获取
     *
     * @param token
     * @param config
     * @param routeId
     * @return
     */
    private Mono<UserInfo> getUserInfoResponseByCache(String token, TokenVerifyConfig config, String routeId) {
        if (!config.getEnableRedisCache()) {
            return Mono.empty();
        }
        return reactiveStringRedisTemplate.opsForValue().get(generateTokenKey(token, config, routeId))
                .onErrorResume(e -> {
                    log.warn("getUserInfoResponseByCache | redis error {}", e.getMessage());
                    return Mono.empty();
                })
                .map(userStr -> {
                    log.info("getUserInfoResponseByCache | redis success str is {}", userStr);
                    UserInfo userInfo = gson.fromJson(userStr, UserInfo.class);
                    return userInfo;
                });

    }

    /**
     * 设置用户信息缓存
     *
     * @param token
     * @param userInfo
     * @param config
     */
    private void createUserInfoCache(String token, UserInfo userInfo, TokenVerifyConfig config, String routeId) {
        if (!config.getEnableRedisCache()) {
            return;
        }
        try {
            stringRedisTemplate.opsForValue().set(generateTokenKey(token, config, routeId), gson.toJson(userInfo), config.getCacheTime());
        } catch (Exception e) {
            log.warn("createUserInfoCache | redis error {}", e.getMessage());
        }
    }

    private String generateTokenKey(String token, TokenVerifyConfig config, String routeId) {
        return Optional.ofNullable(config.getCachePrefix()).orElse("").concat(routeId).concat("xes:token:").concat(token);
    }

    /**
     * 设置header
     *
     * @param userInfo
     * @param exchange
     * @return ServerWebExchange
     */
    private ServerWebExchange installUserInfo(UserInfo userInfo, ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest().mutate()
                .header("ag-xes-stu-id", userInfo.getStuId())
                .header("ag-xes-u-id", userInfo.getUId())
                .header("ag-xes-area", userInfo.getCityCode())
                .header("ag-xes-grade", userInfo.getGradeId())
                .header("ag-xes-tourist-id", Optional.ofNullable(userInfo.getTouristId()).orElse(""))
                .header("ag-xes-user-type", userInfo.isTourist() ? "1" : "0").build();
        return exchange.mutate().request(request).build();
    }

    @AllArgsConstructor
    @Data
    @NoArgsConstructor
    public static class UserInfo {

        /**
         * 是否是游客
         */
        private boolean isTourist;

        private String stuId;

        private String uId;

        private String cityCode;

        private String gradeId;

        private String touristId;


        String getStuId() {
            if (isTourist) {
                return touristId;
            } else {
                return stuId;
            }
        }

    }

    @NoArgsConstructor
    @AllArgsConstructor
    @lombok.Data
    public static class UserInfoResponse {
        private String code;
        private Data data;

        @lombok.Data
        public static class Data {
            @JsonProperty("bindingCode")
            private String bindingCode;
            private Bussion bussion;
            @JsonProperty("areaCode")
            private String areaCode;
            private String grid;
            private String phone;
            private String mphone;
            private String username;
            private String studentId;
            private String id;
            private String email;
            private List<CityList> cityList;
        }


        @lombok.Data
        static class Bussion {
            @JsonProperty("stu_id")
            private String stuId;
            @JsonProperty("stu_uid")
            private String stuUid;
            @JsonProperty("stu_grade_id")
            private String stuGradeId;
            @JsonProperty("stu_loginname")
            private String stuLoginName;
            @JsonProperty("stu_name")
            private String stuName;
            @JsonProperty("stu_sex")
            private int stuSex;
            @JsonProperty("grd_name")
            private String grdName;
            @JsonProperty("stu_grade_type")
            private String stuGradeType;
        }

        @lombok.Data
        @AllArgsConstructor
        @NoArgsConstructor
        static class CityList {
            private String id;
            private String uid;
            private String card;
            private String modifyGridTime;
            private String modifyNameTime;
            @JsonProperty("areaCode")
            private String areaCode;
            private String studentId;
            private int isModifiedGrid;
            private int isModifiedName;
            @JsonProperty("cityName")
            private String cityName;
            @JsonProperty("isActive")
            private int isActive;
        }
    }

    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    private static class CommonResponse<T> {

        private Integer status;
        private String msg;
        private T data;

    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    private static class ValidateParam {

        private String refreshToken;
        @JsonProperty("token")
        private String accessToken;
        @JsonProperty("deviceId")
        private String deviceId;
        @JsonProperty("clientVersion")
        private String clientVersion;
    }
}
